/*
 * License GNU LGPL
 * Copyright (C) 2012 Amrullah <amrullah@panemu.com>.
 */
package com.abc.cheque.ui.form;


import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;

import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.PopupControl;
import javafx.scene.control.Skin;
import javafx.scene.control.Skinnable;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.HBoxBuilder;
import javafx.scene.layout.Priority;

import org.apache.commons.beanutils.PropertyUtils;

import com.abc.cheque.common.TiwulFXUtil;
import com.abc.cheque.common.Validator;

/**
 * This is a parent class of input controls that designed to be used inside
 * {@link Form}. This class simply wraps the input control in order to add new behavior
 * i.e: required icon, invalid icon, invalid message popup.
 * 
 * 
 * @author Amrullah <amrullah@panemu.com>
 */
public abstract class BaseControl<T, C extends Control> extends HBox {

    private String propertyName;
    private BooleanProperty required = new SimpleBooleanProperty(false);
    private BooleanProperty valid = new SimpleBooleanProperty(true);
    private StringProperty errorMessage;
    private static Image imgRequired = new Image(BaseControl.class.getResourceAsStream("/images/required.png"));
    private static Image imginvalid = new Image(BaseControl.class.getResourceAsStream("/images/invalid.png"));
    private static Image imgRequiredInvalid = new Image(BaseControl.class.getResourceAsStream("/images/required_invalid.png"));
    private ImageView imagePlaceHolder = new ImageView();
    private C inputControl;
    protected ObjectProperty<T> value;
    private PopupControl popup;
    private Label errorLabel;
    private List<Validator<T>> lstValidator = new ArrayList<>();
    private InvalidationListener imageListener = new InvalidationListener() {
        @Override
        public void invalidated(Observable o) {
            if (required.get() && !valid.get()) {
                imagePlaceHolder.setImage(imgRequiredInvalid);
            } else if (required.get()) {
                imagePlaceHolder.setImage(imgRequired);
            } else if (!valid.get()) {
                imagePlaceHolder.setImage(imginvalid);
            } else {
                imagePlaceHolder.setImage(null);
            }
        }
    };

    public BaseControl(C control) {
        this("", control);
    }

    public BaseControl(String propertyName, C control) {
        this.inputControl = control;
        this.propertyName = propertyName;
//        Tooltip.install(imagePlaceHolder, tooltip);
//        StackPane.setAlignment(imagePlaceHolder, Pos.CENTER_RIGHT);
        HBox.setHgrow(control, Priority.ALWAYS);
        control.setMaxWidth(Double.MAX_VALUE);
        control.setMinHeight(USE_PREF_SIZE);
        getChildren().add(control);
        getChildren().add(imagePlaceHolder);

        required.addListener(imageListener);
        valid.addListener(imageListener);
//        control.setManaged(true);
        this.getStyleClass().add("form-control");
        value = new SimpleObjectProperty<>();
        bindValuePropertyWithControl(control);
        bindEditablePropertyWithControl(control);

        addEventHandler(MouseEvent.ANY, new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                if (event.getEventType() == MouseEvent.MOUSE_MOVED
                        && !isValid()
                        && !getPopup().isShowing()) {
                    Point2D p = BaseControl.this.localToScene(0.0, 0.0);
                    getPopup().show(BaseControl.this,
                            p.getX() + getScene().getX() + getScene().getWindow().getX(),
                            p.getY() + getScene().getY() + getScene().getWindow().getY() + getInputComponent().getHeight() - 1);
                } else if (event.getEventType() == MouseEvent.MOUSE_EXITED && getPopup().isShowing()) {
                    getPopup().hide();
                }
            }
        });
        getInputComponent().addEventHandler(MouseEvent.MOUSE_ENTERED, new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent t) {
                if (!isValid() && getPopup().isShowing()) {
                    getPopup().hide();
                }
            }
        });
    }

    /**
     * Delegate method. Request focus for underlying input component
     */
    @Override
    public void requestFocus() {
        getInputComponent().requestFocus();
    }
    
    private StringProperty getErrorMessage() {
        if (errorMessage == null) {
            errorMessage = new SimpleStringProperty();
        }
        return errorMessage;
    }

    private PopupControl getPopup() {
        if (popup == null) {
            errorLabel = new Label();
            errorLabel.textProperty().bind(getErrorMessage());
            popup = new PopupControl();
            final HBox pnl = HBoxBuilder.create().children(errorLabel).build();
            pnl.getStyleClass().add("error-popup");
            popup.setSkin(new Skin() {
                @Override
                public Skinnable getSkinnable() {
                    return BaseControl.this.getInputComponent();
                }

                @Override
                public Node getNode() {
                    return pnl;
                }

                @Override
                public void dispose() {
                }
            });
            popup.setHideOnEscape(true);
        }
        return popup;
    }

    /**
     * Sets property name
     * @return 
     */
    public String getPropertyName() {
        return propertyName;
    }

    public void setPropertyName(String propertyName) {
        this.propertyName = propertyName;
    }

    /**
     * Set the field to required. A red star will be shown if this value is true.
     * If the value for this field is empty and required is true, a validation 
     * error will appear on calling {@link Form#validate()}
     * 
     * @param required 
     */
    public void setRequired(boolean required) {
        this.required.set(required);
    }

    public boolean isRequired() {
        return required.get();
    }
    
    public BooleanProperty requiredProperty() {
        return required;
    }

    /**
     * Set the value contained by the control to valid. To set it to invalid
     * call {@link #setInvalid(java.lang.String)}
     */
    public void setValid() {
        this.valid.set(true);
    }

    /**
     * Set the value contained by the control to invalid.
     * @see #setValid() 
     * @param errorMessage 
     */
    public void setInvalid(String errorMessage) {
        this.valid.set(false);
        getErrorMessage().set(errorMessage);
    }

    public boolean isValid() {
        return this.valid.get();
    }

    /**
     * Push value to display in input control
     * @param object 
     */
    public final void pushValue(Object object) {
        try {
            T pushedValue;
            if (!getPropertyName().contains(".")) {
                pushedValue = (T) PropertyUtils.getSimpleProperty(object, propertyName);
            } else {
                pushedValue = (T) PropertyUtils.getNestedProperty(object, propertyName);
            }
            setValue(pushedValue);
        } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
            if (ex instanceof IllegalArgumentException) {
                /**
                 * The actual exception needed to be cathect is
                 * org.apache.commons.beanutils.NestedNullException. But Scene
                 * Builder throw java.lang.ClassNotFoundException:
                 * org.apache.commons.beanutils.NestedNullException if
                 * NestedNullException is referenced in this class. So I catch
                 * its parent isntead.
                 */
                setValue(null);
            } else {
                throw new RuntimeException(ex);
            }
        }
    }

    /**
     * Set value entered to this input control to the passed obj on corresponding
     * property name.
     * @param obj 
     */
    public void pullValue(Object obj) {
        try {
            PropertyUtils.setSimpleProperty(obj, propertyName, this.getValue());
        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Bind {@link #value} with control's specific value property. In case of
     * TextControl it should be
     * <pre>
     * {@code value.bind(inputControl.textProperty())}
     * </pre>
     *
     * @param inputControl underlying input control that is wrapped inside
     * BaseControl
     */
    protected abstract void bindValuePropertyWithControl(C inputControl);
    
    protected void bindEditablePropertyWithControl(C inputControl) {
        inputControl.disableProperty().bind(editableProperty().not());
    };

    public abstract void setValue(T value);

    public final T getValue() {
        return value.get();
    }

    public final ReadOnlyObjectProperty<T> valueProperty() {
        return value;
    }

    /**
     * Gets the underlying input component
     * @return 
     */
    public C getInputComponent() {
        return inputControl;
    }

    /**
     * Validate value contained in the input control. To make the input control
     * mandatory, call {@link #setRequired(boolean true)}
     * @return false if invalid. True otherwise
     * @see #addValidator(com.panemu.tiwulfx.common.Validator) to add validator
     */
    public boolean validate() {
        if (required.get() && (value.get() == null || value.get().toString().equals(""))) {
            String msg = TiwulFXUtil.getLiteral("field.mandatory");
            setInvalid(msg);
            return false;
        }

        for (Validator<T> validator : lstValidator) {
            String msg = validator.validate(getValue());
            if (msg != null && !"".equals(msg)) {
                setInvalid(msg);
                return false;
            }
        }
        setValid();
        return true;
    }

    /**
     * Add validator. An input control might have multiple validators. The validator
     * will be called with the same sequence the validators are added to input controls
     * @param validator 
     */
    public void addValidator(Validator<T> validator) {
        if (!lstValidator.contains(validator)) {
            lstValidator.add(validator);
        }
    }

    public void removeValidator(Validator<T> validator) {
        lstValidator.remove(validator);
    }
    
    /**
     * set whether the input control is editable
     */
    private BooleanProperty editable = new SimpleBooleanProperty(true);
    public void setEditable(boolean editable) {
        this.editable.set(editable);
    }
    public boolean isEditable() {
        return editable.get();
    }
    public BooleanProperty editableProperty() {
        return this.editable;
    }
}
